package Dungeon;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;

import SquarePackage.Direction;
import SquarePackage.Rock;
import SquarePackage.Square;
import SquarePackage.Temperature;
import SquarePackage.TeleportableSquare;
import be.kuleuven.cs.som.annotate.*;

/**
 * Class for rooms. A room is a space in the dungeon, built with squares, which
 * are all attached to eachother.
 * 
 * @invar A room contains at least one square. | getNumberOfBorders()>0
 * @invar The temperature in all the squares is equal each other. |
 *        hasEverywhereCorrectTemperature()
 * @invar The humidity in all the squares is equal each other. |
 *        hasEverywhereCorrectHumidity()
 * 
 * @author Brecht Reynders en Florian Hendrickx
 * @version 2.1
 */
public class Room <E extends Square> {
	////////////////////////////
	///////CONSTURCTORS/////////
	////////////////////////////
	/**
	 * Initializes a new room with a given dungeon, square and dimension.
	 * 
	 * @param dungeon
	 *            The dungeon in which this room is located.
	 * @param square
	 *            First square of this room.
	 * @param dimension
	 *            Position of the square in the dungeon.
	 * @post The given square is part of the new room. | new.contains(square)
	 * @post The given dimension is part of the new room. |
	 *       new.contains(dimension)
	 * @post The given dungeon equals the new dungeon. | new.getDungeon() =
	 *       dungeon
	 */
	protected Room(Dungeon<E> dungeon, E square, Dimension dimension)
			throws IllegalArgumentException {
		positions = new HashMap<Dimension, E>();
		positions.put(dimension, square);
		this.dungeon = dungeon;
		temperature = new Temperature(square.getTemp());
		humidity = square.getHumidity();
		this.area = 1;
	}

	/**
	 * Initializes a new room with a given dungeon and hashmap.
	 * 
	 * @param dungeon
	 *            The dungeon in which this room is located.
	 * @param positions
	 *            Hashmap with one or more dimensions with squares.
	 * @post The given dungeon equals the new dungeon. | new.getDungeon() =
	 *       dungeon
	 * @post The given positions equal the positions. | new.getPositions() =
	 *       positions
	 */
	protected Room(Dungeon<E> dungeon, Map<Dimension,E> positions)
			throws IllegalArgumentException {
		this.dungeon = dungeon;
		this.positions = positions;
		List<E> list = new ArrayList<E>(positions.values());
		this.temperature = new Temperature(list.get(0).getTemp());
		this.humidity = list.get(0).getHumidity();
		this.area = positions.size();
	}
	
	////////////////////////////
	////////VARIABELEN//////////
	////////////////////////////
	/**
	 * Variable registering all the dimensions and Squares in this room.
	 */
	private Map<Dimension, E> positions;

	/**
	 * Variable registering the dungeon in which the room is located.
	 */
	private final Dungeon<E> dungeon;

	/**
	 * Variable registering whether this room is terminated.
	 */
	private boolean isTerminated = false;

	/**
	 * Variable registering the temperature in this room.
	 */
	private Temperature temperature;

	/**
	 * Variable registering the humidity in this room.
	 */
	private BigDecimal humidity;
	
	/**
	 * Variable registering the total area of this room.
	 */
	private int area;
	
	////////////////////////////////
	//////GETTERS AND SETTERS///////
	////////////////////////////////
	/**
	 * Return the dungeon in which this room is located.
	 */
	@Basic
	protected final Dungeon<E> getDungeon() {
		return dungeon;
	}

	/**
	 * Return whether this room is terminated or not.
	 */
	@Basic
	@Raw
	protected boolean isTerminated() {
		return isTerminated;
	}

	/**
	 * Return the humidity of this Room.
	 */
	@Basic
	@Raw
	protected BigDecimal getHumidity() {
		return humidity;
	}

	/**
	 * Return the temperature of this Room.
	 */
	@Basic
	@Raw
	protected Temperature getTemperature() {
		return temperature;
	}

	/**
	 * Return an hashmap with all the dimensions and squares of this room.
	 */
	@Basic
	@Raw
	protected Map<Dimension, E> getPositions() {
		return positions;
	}

	/**
	 * Return a collection of all the squares in this room .
	 */
	@Raw
	protected Collection<E> getAllSquares() {
		return getPositions().values();
	}

	/**
	 * Returns a set of all the dimensions in this room.
	 */
	protected Set<Dimension> getAllDimensions() {
		Set<Dimension> set = getPositions().keySet();
		return set;
	}

	/**
	 * Return the number of squares in this room.
	 */
	protected int getNumberOfSquares() {
		return getAllSquares().size();
	}
	
	/**
	 * Return the area of the whole room.
	 */
	protected int getArea() {
		return area;
	}
	
	/**
	 * Sets the area of this room to the given value.
	 * @param 	area
	 * 			The new area of this room.
	 * @post	The given area is the new area.
	 * 			new.getArea() = area
	 * @throws	IllegalArgumentException
	 * 			The given area is smaller than zero.
	 * 			| area < 0
	 */
	protected void setArea(int area) throws IllegalArgumentException {
		if(area < 0)
			throw new IllegalArgumentException(area + " is smaller than zero.");
		this.area = area;
	}

	/**
	 * Returns the square on the given dimension.
	 * 
	 * @param 	dimension
	 *          The dimension of which the square must be returned.
	 * @return 	The square at the given dimension. 
	 * 			| positions.get(dimension)
	 */
	protected E getSquareAt(Dimension dimension) {
		return positions.get(dimension);
	}

	/**
	 * Returns the dimension of the given square.
	 * 
	 * @param 	square
	 *        	The square of which we want to know the dimension.
	 * @return 	The dimension of the square. 
	 * 			| for each dimension in getAllDimensions()
	 *         	|	square2 = getPositions().get(dimension)
	 *          | 	if square == square2
	 *          |		result = dimension
	 *          | result = null 
	 */
	protected Dimension getDimensionOf(Square square) {
		for (Dimension dimension : getAllDimensions()) {
			if (getPositions().get(dimension).equals(square))
				return dimension;
		}
		return null;
	}

	/**
	 * Return all the squares in the whole room, this is the room with all the square, which have no isolation between them.
	 * 
	 * @param 	dimension
	 * 			The starting dimension, the dimension of which is sure that it is in this room.
	 * @return	All the squares that are in the same room as the given dimension.
	 * 			| Set squares.contains(getSquareOf(dimension))&& 
	 * 			| for each square in squares
	 * 			| 	squares.addAll(square.getNeighbours)
	 * 			| result = squares
	 */
	protected Set<Square> getAllSquaresInWholeRoom(){ 
		Dimension dimension = (Dimension) getAllDimensions().toArray()[0];
		Set<Square> squares = new HashSet<Square>();
		List<Square> iterator = new ArrayList<Square>();
		iterator.add(getSquareAt(dimension));
		int i = 0;
		while(i < iterator.size()){
			for(Square square : iterator.get(i).getNeighbours()){
				if(!iterator.contains(square)){
					iterator.add(square);
				}
			}
			i++;
		}
		squares.addAll(iterator);
		return squares;
	}
	
	////////////////////////////////////////
	//////FIND SQUARES IN THIS DUNGEON//////
	////////////////////////////////////////
	/**
	 * Checks whether the given square is in the same whole room as the given dimension.
	 * 
	 * @param 	square
	 * 			The square to find.
	 * @return	True if and only if the square is in the large room.
	 * 			| getAllSquaresInWholeRoom(dimension).contains(square)
	 */
	protected boolean containsSquare(Square square){
		return getAllSquaresInWholeRoom().contains(square);
	}
	
	/**
	 * Checks whether this part of the whole room contains the given square.
	 * 
	 * @param 	square
	 *          The square to search in this room.
	 * @return 	True if and only if the given square is part of this room.
	 *         	| result = getPositions.containsValue(square)
	 */
	protected boolean contains(Square square) {
		return getPositions().containsValue(square);
	}

	/**
	 * Check whether this room contains the given dimension.
	 * 
	 * @param 	dimension
	 *          The dimension to search in this square.
	 * @return 	True if and only if the given dimension is part of this room.
	 *         	| result = getPositions.containsKey(dimension)
	 */
	protected boolean contains(Dimension dimension) {
		return getPositions().containsKey(dimension);
	}
	
	////////////////////////////
	////////MERGING ROOMS///////
	////////////////////////////
	/**
	 * Merge this room with the given room.
	 * 
	 * @param 	room
	 *          The room with which this room should merge.
	 * @post 	This square contains the squares of both of the rooms.
	 *       	| this.getAllSquares() + room.getAllSquares()= new.getAllSquares()
	 * @effect 	The given room is empty and gets removed.
	 *         	| !new.getRooms().contains(room)
	 * @effect	The area of the whole room is equal.
	 * 			| updateArea()
	 * @effect 	The temperature and humidity are equal in all the squares of this
	 *         	room. 
	 *         	| quickUpdateTemperatureAndHumidity()
	 */
	protected void mergeWith(Room<?> room) {
		if(!room.getDungeon().equals(getDungeon()))
			externalMergeWith(room);
		else if(!this.equals(room)){
			positions.putAll(((Room<E>)room).getPositions());
			quickUpdateTemperatureAndHumidity(room);
			updateArea();
			getDungeon().removeDoubleRoom(((Room<E>)room), this);
		}
	}
	
	/**
	 * Merges this room with the given room in another dungeon.
	 * 
	 * @param 	room
	 * 			Room in another dungeon.
	 * @effect	The area of the whole room is equal.
	 * 			| updateArea()
	 * @effect 	The temperature and humidity are equal in all the squares of this
	 *         	room. 
	 *         	| quickUpdateTemperatureAndHumidity()
	 */
	protected void externalMergeWith(Room<?> room){
//		area += room.getArea();
//		if(getArea()!=room.getArea()){
//			room.externalMergeWith(this);
//		}
		quickUpdateTemperatureAndHumidity(room);
		updateArea();
	}

	/**
	 * Update the temperature and humidity in this room, so that the humidity
	 * and the temperature in all the squares are equal. Only used for rocks and rooms of size 1.
	 * 
	 * @post 	The temperature in all the squares equal each other. 
	 * 			| hasEverywhereCorrectTemperature()
	 * @post 	The humidity in all the squares equal each other. 
	 * 			| hasEverywhereCorrectHumidity()
	 */
	protected void updateTemperatureAndHumidity() {
//		Set <Square> squares = getAllSquaresInWholeRoom((Dimension)getAllDimensions().toArray()[0]);  
//		if (!hasEverywhereCorrectHumidity(squares)||(!hasEverywhereCorrectTemperature(squares))) {
//			BigDecimal newHumidity = new BigDecimal("0.00");
//			double newTemperature = 0.0;
//			for (Square square : squares) {
//				newHumidity = newHumidity.add(square.getHumidity());
//				newTemperature += square.getTemp();
//			}
//			BigDecimal divisor = new BigDecimal(getNumberOfSquares());
//			newTemperature = newTemperature / getNumberOfSquares();
//			newHumidity = newHumidity.divide(divisor, 2, RoundingMode.HALF_UP);
//			for (Square square : squares) {
//				((TeleportableSquare) square).setHumidity(newHumidity);
//				((TeleportableSquare) square).setTemperature(newTemperature);
//			}
//			this.humidity = newHumidity;
//			this.temperature.setTemperature(newTemperature);
//		}
		if (getAllSquares().toArray()[0] instanceof Rock) {
			((Rock) getAllSquares().toArray()[0]).updateTemperature();
			this.temperature = new Temperature(((Square)getAllSquares().toArray()[0]).getTemp());
		}
	}
	
	/**
	 * Updates the temperature and humidity for merging rooms.
	 * 
	 * @param 	room
	 * 			The room to merge with.
	 * @post	The new temperature equals the temperature in all the squares and in this and the given room.
	 * 			| new.temperature = (getArea()*getTemperature()+room.getArea()*room.getTemperature())/(getArea()+room.getArea())
	 * @post	The new humidity equals the temperature in all the squares and in this and the given room.
	 * 			| new.humidiy = (getArea()*getHumidity()+room.getArea()*room.getHumidity())/(getArea()+room.getArea())
	 */
	protected void quickUpdateTemperatureAndHumidity(Room<?> room){
		Set<Square>squares = getAllSquaresInWholeRoom();
		if(!(hasEverywhereCorrectHumidity(squares)&&hasEverywhereCorrectTemperature(squares))){
			double newTemperature = (room.getTemperature().getTemperature()*room.getArea()+getTemperature().getTemperature()*getArea())/(room.getArea()+getArea());
			BigDecimal newHumidity = room.getHumidity().multiply(new BigDecimal(room.getArea())).add(getHumidity().multiply(new BigDecimal(getArea()))).divide(new BigDecimal(getArea() + room.getArea()), 3, RoundingMode.HALF_UP);
			for (Square square : squares) {
				((TeleportableSquare) square).setHumidity(newHumidity);
				((TeleportableSquare) square).setTemperature(newTemperature);
			}
			getDungeon().getRootDungeon().updateRocks();
		}
	}
	
	/**
	 * Checks whether this room has everywhere the correct temperature.
	 * 
	 * @param	squares 
	 * 			The squares in the whole room.
	 * @return 	True if and only if the temperature in all the squares equal each other.
	 *     		| for each square in squares
	 *     		| 	result = square.getTemp() == squares.toArray()[0].getTemp()
	 */
	private boolean hasEverywhereCorrectTemperature(Set<Square> squares) {
		if (getPositions().size() == 1) {
			return true;
		}
		for (Square square : squares) {
			if (square.getTemp() != ((Square)squares.toArray()[0]).getTemp())
				return false;
		}
		return true;
	}
	
	/**
	 * Checks whether this room has everywhere the correct humidity.
	 * 
	 * @param	squares 
	 * 			The squares in the whole room.
	 * @return 	True if and only if the humidity in all the squares equal each other.
	 *     		| for each square in squares
	 *     		| 	result = square.getHumidity() == squares.toArray()[0].getHumidity()
	 */
	private boolean hasEverywhereCorrectHumidity(Set<Square> squares) {
		if (getPositions().size() == 1) {
			return true;
		}
		for (Square square : squares) {
			if (!square.getHumidity().equals(
					((Square)squares.toArray()[0]).getHumidity()))
				return false;
		}
		return true;
	}
	
	/**
	 * Search the area of this room in this and the other dungeons.
	 * 
	 * @post	This room has the right area.
	 * 			| new.getArea()=getAllSquaresInTheWholeRoom().size()
	 * @post	The external room has a correct area.
	 * 			| new.room.getArea()=new.getArea()
	 */
	protected void updateArea()throws IllegalArgumentException{
		Set<Square> squares = getAllSquaresInWholeRoom();
		int totalArea = squares.size();
		for(Square square : squares){
			getDungeon().getRootDungeon().getRoomOf(square).setArea(totalArea);
		}
	}

	/////////////////////////////////////////////////////////////////
	////////RECURSIVE METHODS FOR FINDING SQUARES IN THIS ROOM///////
	/////////////////////////////////////////////////////////////////
	/**
	 * Returns a map of dimensions and squares which are in the same room.
	 * 
	 * @param 	dimension
	 *          The dimension of a square which is certainly in the room to return.
	 * @return 	A map of dimensions and squares which are all in the same room. 
	 * 			| result = getNeighbourSquares(dimension, null)
	 */
	protected Map<Dimension, E> getNeighbourSquares(Dimension dimension) {
		Map<Dimension, E> neighbourSquares = new HashMap<Dimension, E>();
		return getNeighbourSquares(dimension, neighbourSquares);
	}

	/**
	 * Returns a map of dimensions and squares which are in the same room.
	 * 
	 * @param 	dimension
	 *          The dimension of a square which is certainly in the room to return.
	 * @param 	neighbourSquares
	 *          The dimensions and squares which are already found.
	 * @return 	A map of dimensions and squares which are all in the same room.
	 * 			| neighbourSquares.put(dimension.getSquareAt(dimension)
	 * 			| for each otherDimension in getAllDimension{	
	 * 			| 	if(isValidNeighbourSquare(dimension,otherDimension,neighbourSquares)
	 * 			| 		getNeighbourSquares(other, neighbourSquares)
	 * 			| }
	 * 			| result = getNeighbourSquares;
	 */
	private Map<Dimension, E> getNeighbourSquares(Dimension dimension,
			Map<Dimension, E> neighbourSquares) {
		neighbourSquares.put(dimension, getSquareAt(dimension));
		for (Dimension other : getAllDimensions()) {
			if (isValidNeighbourSquare(dimension, other, neighbourSquares)) {
				getNeighbourSquares(other, neighbourSquares);
			}
		}
		return neighbourSquares;
	}

	/**
	 * Checks whether the other dimension is a valid dimension.
	 * 
	 * @param dimension
	 *            The dimension to find the neighbours.
	 * @param other
	 *            The second dimension to verify if it's a valid neighbour.
	 * @param neighbourSquares
	 *            The dimensions and squares that are already found.
	 * @return True if and only if the dimensions are neighbours and the
	 *         dimension isn't a key already and the borders between the
	 *         dimensions are open. | return = dimension.isNeighbour(other) &&
	 *         !neighbourSquares.containsKey(other) &&
	 *         !getSquareAt(dimension).getBorderAt
	 *         (dimension.getNeighbourDirection(other)).getIsolation()
	 * @note If the return is false, it doesn't mean that the other dimension is
	 *       not a valid dimension.
	 */
	private boolean isValidNeighbourSquare(Dimension dimension,
			Dimension other, Map<Dimension, E> neighbourSquares) {
		if (!(dimension.isNeigbour(other) && !neighbourSquares
				.containsKey(other)))
			return false;
		return !getSquareAt(dimension).getBorderAt(
				dimension.getNeigbourDirection(other)).getIsolation();
	}

	/**
	 * Returns a neighbouring dimension of this room.
	 * 
	 * @param dimension
	 *            The dimension of which a neighbouring dimension is needed.
	 * @return A neighbouring dimension or null, if the room contains just one
	 *         square. | result =
	 *         dimension(if(getPositions().containsKey(dimension))) else(null)
	 */
	protected Dimension getNeighbourPosition(Dimension dimension) {
		for (Direction direction : Direction.values()) {
			if (getPositions().containsKey(dimension.getDimensionIn(direction)))
				return dimension.getDimensionIn(direction);
		}
		return null;
	}
	
	////////////////////////////////
	///////REMOVING SQUARES/////////
	////////////////////////////////
	/**
	 * Terminates this room.
	 * 
	 * @post	All the squares in this room get removed.
	 * 			| for each dimension in getAllDimensions()
	 * 			|	removeSquareAt(dimension)
	 * @post	The terminated state of this room is true.
	 * 			| new.isTerminated() = true
	 */
	protected void terminate() {
		for (Dimension dimension : getAllDimensions()) {
			removeSquareAt(dimension);
		}
		isTerminated = true;
	}
	
	/**
	 * Remove a square at the given dimension.
	 * 
	 * @param 	dimension
	 *          The dimension on which the square must be removed.
	 * @post 	The square isn't a square of this room anymore.
	 *       	| new.getSquareAt(dimension) = null
	 * @effect 	The square is terminated.
	 *         	| this.getSquareAt(dimension).isTerminated()
	 * @post	The new area is one smaller than the previous area.
	 * 			| new.area = area-1
	 * @post	If the amount of squares in this room is 0, the room get terminated.
	 * 			| if(getNumberOfSquares() == 0)
	 * 			|    this.terminate()
	 */
	protected void removeSquareAt(Dimension dimension) {
		getSquareAt(dimension).terminate();
		positions.remove(dimension);
		area -= 1;
		if(getNumberOfSquares()==0)
			terminate();
	}
	
	/**
	 * Removes the squares that are transferred to the given room.
	 * 
	 * @param 	room
	 *          Room that contains some squares which are also in this room.
	 * @post 	This room and the given room don't contain the same squares
	 *       	anymore.
	 *       	| removeDoubleAt(dimension)
	 */
	protected void removeDoubleSquares(Room<E> room) {
		for (Dimension dimension : getAllDimensions()) {
			room.removeDoubleAt(dimension);
		}
	}

	/**
	 * Removes a double square at the given dimension.
	 * 
	 * @param 	dimension
	 *          The dimension on which there is a double room.
	 * @post 	The square at the given dimension isn't part of 2 rooms anymore. This dungeon doesn't contain a square at the given dimension anymore.
	 * 			| !this.contain(dimension)
	 */
	private void removeDoubleAt(Dimension dimension) {
		positions.remove(dimension);
		area -= 1;
		if(getNumberOfSquares()==0)
			terminate();
	}

	/////////////////////////////////////////
	///////EXTRA PART FOR MAIN PROGRAM///////
	/////////////////////////////////////////
	/**
	 * Return the humidity in percentage form help method for toString().
	 * 
	 * @return 	humidity followed by a percentage sign "%"
	 */
	@Raw
	private String getHumidityPercentage() {
		NumberFormat myFormatter = new DecimalFormat("##,##");
		myFormatter.setMinimumFractionDigits(2);
		myFormatter.setMaximumIntegerDigits(3);
		DecimalFormatSymbols commaToPointChanger = new DecimalFormatSymbols();
		commaToPointChanger.setDecimalSeparator('.');
		((DecimalFormat) myFormatter)
				.setDecimalFormatSymbols(commaToPointChanger);
		String output = "" + myFormatter.format(getHumidity()) + "%";
		return output;
	}
	
	@Override
	public String toString(){
		 return "Temperature: " + getTemperature() +'\n'+
	                "Humidity: "+ getHumidityPercentage() + '\n' + "Area: " + 
	                getArea()+ '\n';
	}
	
//	 /**
//	 * Check whether this room equals the given room.
//	 *
//	 * @param room
//	 * The room to check whether it is equal or not.
//	 * @return True if and only if the given room equals this room.
//	 */
//	 @Override
//	 public boolean equals(Object room){
//	 if(room == null)
//	 return false;
//	 if(room.getClass()!= Room.class)
//	 return false;
//	 if(!getPositions().equals(((Room<?>) room).getPositions()))
//	 return false;
//	 return true;
//	 }
}
